﻿// UltraTextWriter.cs
//
// Copyright 2008 by Burt Harris
// Licensed under the Microsoft Public License (Ms-PL)
// See: http://www.codeplex.com/yaml/license
//
using System;
using System.Collections;
using System.IO;
using System.Text;

namespace YaTools.Yaml
{
    sealed internal class UltraTextWriter : TextWriter
    {
        //
        // The heart of this class is the logic in the Write(char) method, 
        // which performs indentation, line folding, and characater escaping 
        // for YAML and YAML-like (JSON) output.  Thus it acts as the 
        // presentation layer in the YAML processing model.
        //
        int _position, _indent, _wrap;
        char _lastChar;
        BitArray _printable;
        TextWriter _inner;
        Scalar.Style _style = Scalar.Style.Default;

        //
        // Conservative default set of printable characters
        //
        static BitArray s_7bit = new BitArray(new[] { 0, -1, -1, -2 });
        static BitArray s_8bit = new BitArray(new[] { 0, -1, -1, -2, 0, -1, -1, -1 });

        /// <summary>
        /// Construct a new UltraTextWriter
        /// </summary>
        /// <param name="innerTextWriter">Inner TextWriter output will be sent to.</param>
        public UltraTextWriter(TextWriter innerTextWriter)
        {
            _inner = innerTextWriter;
            if (innerTextWriter.Encoding.IsSingleByte)
                _printable = s_7bit;                    // ASCII etc.
            else
                _printable = s_8bit;                    // UTF-8, UTF-16 etc.
        }

        /// <summary>
        /// Last column written to on the current line.  This will be zero at the beginning
        /// of any new line, unless Indent is non-zero.
        /// </summary>
        public int Column
        {
            get { return _position; }
        }

        /// <summary>
        /// Amount of indent for new lines.  
        /// </summary>
        public int Indent
        {
            get { return _indent; }
            set
            {
                if (value < 0) 
                    throw new ArgumentOutOfRangeException("value");
                _indent = value;
            }
        }

        /// <summary>
        /// Controls representation of special characters
        /// </summary>
        internal Scalar.Style Style
        {
            get { return _style; }
            set { _style = value; }
        }


        /// <summary>
        /// Word wrapping target width.   
        /// </summary>
        public int Wrap
        {
            get { return _wrap; }
            set
            {
                if (value < 0) 
                    throw new ArgumentOutOfRangeException("value");
                _wrap = value;
            }
        }

        /// <summary>
        /// Determines if a given character is considered "Printable", or must be 
        /// escaped using hexadecimal.
        /// </summary>
        public bool IsPrintable(char ch)
        {
            int i = (int)ch;
            if (i < 0 || i >= _printable.Length)
                return false;
            return _printable[i];
        }

        /// <summary>
        /// Writes a character to the output stream, with processing...
        /// </summary>
        /// <param name="c">Character to write</param>

        public sealed override void Write(char c)
        {
            switch (c)
            {
                case Unicode.CR:
                    WriteLine();
                    _lastChar = Unicode.CR;
                    return;

                case Unicode.LF:
                    if (_lastChar != Unicode.CR)
                        WriteLine();
                    return;
            }

            switch (_lastChar)
            {
                case Unicode.CR:
                case Unicode.LF:
                    _inner.WriteSpaces(Indent);
                    _position += Indent;
                    _lastChar = Unicode.Space;
                    break;

                case Unicode.Space:
                    // Folding opportunity
                    if (_wrap > 0 && _position >= _wrap && _style != Scalar.Style.Literal
                        && !Char.IsWhiteSpace(c))
                    {
                        _inner.WriteLine();
                        _inner.WriteSpaces(Indent);
                        _position = Indent;
                        _lastChar = Unicode.Space;
                    }
                    break;
            }

            switch (_style)
            {
                default:    // No escaping possible, so we trust the caller's judgement
                    WriteRaw(c);
                    break;

                case Scalar.Style.SingleQuoted:
                    if (c == Unicode.SingleQuote)
                    {
                        WriteRaw(c);
                    }
                    WriteRaw(c);
                    break;

                case Scalar.Style.DoubleQuoted:

                    if (IsPrintable(c))
                    {
                        // If control codes are marked printable it means
                        // we should use a predefined escape for them.  This
                        // lets the distinction between writing YAML and JSON escapes
                        // be encoded in the _printable array.
                        switch (c)
                        {
                            case Unicode.Backslash:
                            case Unicode.DoubleQuote: Backslash(c); return;
                            case '\0': Backslash('0'); return;
                            case '\a': Backslash('a'); return;
                            case '\b': Backslash('b'); return;
                            case Unicode.Escape: Backslash('e'); return;
                            case '\f': Backslash('f'); return;
                            case '\n': Backslash('n'); return;
                            case '\r': Backslash('r'); return;
                            case '\t': Backslash('t'); return;
                            case '\v': Backslash('v'); return;
                            case Unicode.NEL: Backslash('N'); return;
                            case Unicode.NBS: Backslash('_'); return;
                            case Unicode.LS: Backslash('L'); return;
                            case Unicode.PS: Backslash('P'); return;
                            default: WriteRaw(c); return;
                        }
                    }
                    else if (c < (char)0x100) // Use Hex notation
                    {
                        _inner.Write("\\x{0;X2}", (int)c);
                        _position += 4;
                    }
                    else if (Char.IsSurrogate(c)) // Use 32-bit character notation
                    {
                        if (Char.IsSurrogatePair(_lastChar, c))
                        {
                            _inner.Write("\\U{0;X8}", Char.ConvertToUtf32(_lastChar, c));
                            _position += 10;
                        }
                        else
                        {
                            _lastChar = c; // Save for later, without printing
                        }
                    }
                    else // Use 16-bit Unicode notation
                    {
                        _inner.Write("\\u{0;X4}", (int)c);
                        _position += 6;
                    }
                    break;
            }
        }


        /// <summary>
        /// Writes a backslash escaped character code
        /// </summary>
        /// <param name="value">Character to write</param>
        private void Backslash(char value)
        {
            _inner.Write(Unicode.Backslash);
            _inner.Write(_lastChar = value);
            _position += 2;
        }

        /// <summary>
        /// Writes a raw character to the output
        /// </summary>
        private void WriteRaw(char value)
        {
            _inner.Write(value);
            _lastChar = value;

            // Account for the character in the running count
            if (IsBreak(value)) 
                _position = 0;
            else 
                _position++; 
        }

        private void WriteRaw(string value)
        {
            if (value != null)
            {
                _inner.Write(value);
                _lastChar = value[value.Length - 1];

                // Account for the string in the running position

                int i = LastIndexOfBreak(value);
                if (i == -1)
                    _position += value.Length;
                else
                    _position = value.Length - i - 1;
            }
        }

        /// <summary>
        /// Writes a line break into to the text using whatever convention is approprate.
        /// </summary>
        public override void WriteLine()
        {
            switch (_style)
            {
                case Scalar.Style.DoubleQuoted:
                    _inner.Write("\\n");
                    break;

                case Scalar.Style.Literal:
                    _inner.WriteLine();     // Line breaks preserved in literal scalars
                    break;

                default:
                    _inner.WriteLine();     // Others implement folding...
                    _inner.WriteLine();
                    break;
            }
            _lastChar = Unicode.LF;
            _position = 0;
        }

        /// <summary>
        /// Returns the System.Text.Encoding being used by the inner TextWriter.   
        /// </summary>
        public override Encoding Encoding
        {
            get { return _inner.Encoding; }
        }

        /// <summary>
        /// Write a directive, document start or document end, which
        /// must appear at the beginning of a line.
        /// </summary>
        /// <param name="directive">Text to write, e.g. "%YAML"</param>
        internal void WriteDirective(string directive)
        {
            if (directive != null)
            {
                if (_position != 0)
                    WriteLine();
                WriteRaw(directive);
            }
        }

        #region Internal character classificaiton functions

        private static bool IsBreak( char ch )
        {
            return (ch == Unicode.CR || ch == Unicode.LF);
        }

        private static int LastIndexOfBreak(string value)
        {
            return Math.Max(
                value.LastIndexOf(Unicode.CR),
                value.LastIndexOf(Unicode.LF));
        }
        #endregion
    }
}
